﻿#region Disclaimer/Info
///////////////////////////////////////////////////////////////////////////////////////////////////
// MetaCollection
// 
// MetaCollection is a system to provide a standard structure, permissions and metadata service
// for content organization.
//
// This project is hosted at Google Code at http://code.google.com/p/metacollection/
//
// This project is licensed under the BSD license.  See the License.txt file for more information.
///////////////////////////////////////////////////////////////////////////////////////////////////
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml;
using System.Xml.Linq;
using MetaCollection.Framework;
using MetaCollection.Framework.Model;
using Model = MetaCollection.Framework.Model;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Lucene.Net.Analysis.Standard;
using System.Collections;

namespace MetaCollection.ContentService.WebDav.Methods
{
	/// <summary>
	/// This controller returns the supported http methods for the given URL.
	/// See http://greenbytes.de/tech/webdav/rfc5323.html for more information.
	/// </summary>
	/// <example>
	/// >> Request:
	/// 
	/// SEARCH / HTTP/1.1
	/// Host: example.org
	/// Content-Type: application/xml; charset="utf-8" 
	/// Content-Length: 252
	/// 
	/// <?xml version="1.0" encoding="UTF-8"?>
	/// <D:searchrequest xmlns:D="DAV:" xmlns:F="http://example.com/foo">
	///   <F:natural-language-query>
	///     Find the locations of good Thai restaurants in Los Angeles
	///   </F:natural-language-query>
	/// </D:searchrequest>
	/// 
	/// >> Response:
	/// 
	/// HTTP/1.1 207 Multi-Status
	/// Content-Type: text/xml; charset="utf-8"
	/// Content-Length: 429
	/// 
	/// <?xml version="1.0" encoding="UTF-8"?>
	/// <D:multistatus xmlns:D="DAV:"
	///    xmlns:R="http://example.org/propschema">
	///   <D:response>
	///     <D:href>http://siamiam.example/</D:href>
	///     <D:propstat>
	///       <D:prop>
	///          <R:location>259 W. Hollywood</R:location>
	///         <R:rating><R:stars>4</R:stars></R:rating>
	///       </D:prop>
	///       <D:status>HTTP/1.1 200 OK</D:status>
	///     </D:propstat>
	///   </D:response>
	/// </D:multistatus>
	/// 
	///>> Response: (truncacted)
	///
	///HTTP/1.1 207 Multistatus
	///Content-Type: text/xml; charset="utf-8"
	///Content-Length: 640
	///
	///<?xml version="1.0" encoding="utf-8"?>
	///<D:multistatus xmlns:D="DAV:">
	///  <D:response>
	///    <D:href>http://www.example.net/sounds/unbrokenchain.au</D:href>
	///    <D:status>HTTP/1.1 200 OK</D:status>
	///  </D:response>
	///  <D:response>
	///    <D:href>http://tech.mit.example/arch96/photos/Lesh1.jpg</D:href>
	///    <D:status>HTTP/1.1 200 OK</D:status>
	///  </D:response>
	///  <D:response>
	///    <D:href>http://example.net</D:href>
	///    <D:status>HTTP/1.1 507 Insufficient Storage</D:status>
	///    <D:responsedescription xml:lang="en">
	///       Only first two matching records were returned
	///    </D:responsedescription>
	///  </D:response>
	///</D:multistatus>
	/// </example>
	public static class Search
	{
		const int MAX_RESULT = 200;

		public static void Respond(DavContext context, IRequestHandler handler)
		{
			IItemInstance searchItem = new ItemInstance();
			IEnumerable<XElement> searchProperties;
			List<XName> responseProperties;
			context.Response.SetCacheHeaders(HttpCacheability.ServerAndPrivate, DateTime.Now);
			GetRequestedProperties(context.Request.Xml, out searchProperties, out responseProperties);
			var query = BuildQuery(searchProperties);
			var searcher = RequestHandlerFactory.CreateIndexSearcher();
			var results = searcher.Search(query, null, 1000).scoreDocs;
			
			context.Response.OutputXml(
				ProcessResults(results.GetEnumerator(), searcher)
					.DavData(Model.LoadOptions.ItemComplete)
					.PropInfoResponse(context, responseProperties.Count < 1 ? Properties.BasicNames : responseProperties));

			// too bad the java port means that they don't implement IDisposable... perhaps I should build a wrapper to call close...
			searcher.Close();
		}

		/// <summary>
		/// Gets the requested properties.
		/// </summary>
		/// <param name="requestXml">The request XML.</param>
		/// <param name="searchProperties">The properties to search for items with similar values.</param>
		/// <param name="requestedProperties">The requested properties that should be included in a response.</param>
		static void GetRequestedProperties(XElement requestXml, out IEnumerable<XElement> searchProperties, out List<XName> requestedProperties)
		{
			requestedProperties = new List<XName>();
			searchProperties = new List<XElement>();

			if (requestXml != null)
			{
				var propNames = requestXml.Elements(XName.Get("prop", "DAV:")).FirstOrDefault();
				var includeName = requestXml.Elements(XName.Get("include", "DAV:")).FirstOrDefault();

				if (includeName != null)
					requestedProperties = requestedProperties.Concat(from n in includeName.Elements() where n.NodeType == XmlNodeType.Element select n.Name).ToList();
				if (propNames != null)
					searchProperties = from n in propNames.Elements() where n.NodeType == XmlNodeType.Element select n;
			}
		}

		/// <summary>
		/// Takes a lucene.net results enumerator that returns Hits and 
		/// processes the hit document into an ItemInstance
		/// </summary>
		/// <param name="results">a query result set</param>
		/// <returns>a list of iteminstances found</returns>
		static IEnumerable<IItemInstance> ProcessResults(IEnumerator results, Searcher searcher)
		{
			int count = 0;
			if (results == null)
				yield break;
			while (results.MoveNext())
			{
				Lucene.Net.Documents.Document hit = searcher.Doc((results.Current as ScoreDoc).doc);
				if (hit != null)
				{
					ItemInstance item = new ItemInstance();
					item.Id = new Guid(hit.Get("Id"));
					if (!string.IsNullOrWhiteSpace(hit.Get("ParentId")))
						item.ParentId = new Guid(hit.Get("ParentId"));
					item.Specification = new Guid(hit.Get("Specification"));
					item.ItemClaimId = new Guid(hit.Get("ItemClaimId"));
					item.LastModified = DateTime.Parse(hit.Get("LastModified"));
					item.MimeType = hit.Get("MimeType");
					item.Name = hit.Get("Name");
					if (!string.IsNullOrWhiteSpace(hit.Get("Href")))
						item.Href = new Uri(hit.Get("Href"));
					if (!string.IsNullOrWhiteSpace(hit.Get("Published")))
						item.Published = Boolean.Parse(hit.Get("Published"));
					item.Version = Int32.Parse(hit.Get("Version"));
					yield return item;
					if (count++ > MAX_RESULT)
						yield break;
				}
			}
		}
		/// <summary>
		/// Looks at all of the set properties and creates a query looking for those values.
		/// </summary>
		/// <param name="searchProperties">the properties to search for</param>
		/// <returns>a lucene query that searches for those properties</returns>
		static Query BuildQuery(IEnumerable<XElement> searchProperties)
		{
			var standardLuceneAnalyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29);

			BooleanQuery query = new BooleanQuery();
			foreach (var prop in searchProperties)
			{
				var parser = new QueryParser(Lucene.Net.Util.Version.LUCENE_29, prop.Name.LocalName, standardLuceneAnalyzer);
				try
				{
					query.Add(parser.Parse(prop.Value), BooleanClause.Occur.SHOULD);
				}
				catch (Lucene.Net.QueryParsers.ParseException) // if the user put in something unexpected then retry with an escaped version.
				{
					query.Add(parser.Parse(QueryParser.Escape(prop.Value)), BooleanClause.Occur.SHOULD);
				}
			}
			return query;
		}
		/*
<?xml version="1.0" encoding="utf-8" ?>
<D:search xmlns:D="DAV:" xmlns:MC="uri://MetaCollection/v2">
		<D:prop>
			    <MC:Notes>test</MC:Notes> 
		</D:prop>
		<D:include>
				<MC:name/>
				<MC:mimetype/>
				<MC:lastmodified/>
				<MC:id/>
				<MC:parentid/>
				<MC:version/>
				<MC:published/>
				<MC:specification/>
		</D:include>
</D:search>
		 */
	}
}
